세마포어와 동시성 제어
NOTE
세마포어가 동시성 제어 도구이면서 동시에 IPC로 분류되는 이유, 스레드용 도구와 프로세스용 도구가 갈리는 기준, System V 세마포어 기본 API를 정리한다.
1. 세마포어의 본질: 동시성 제어
세마포어는 원래 여러 실행 흐름이 공유 자원에 접근할 때 순서를 맞추는 카운터/락 메커니즘입니다. 뮤텍스, 조건변수와 같은 계열이라고 보면 됩니다.
2. 근데 왜 IPC로 분류되나
동시성 제어가 필요한 대상이 어디에 있느냐에 따라 도구가 갈립니다.
| 대상 | 도구 | IPC 여부 |
|---|---|---|
| 한 프로세스 안의 여러 스레드 | pthread_mutex, pthread_cond, 이름없는 세마포어(sem_init, pshared=0) | 아님 — 같은 주소공간이라 그냥 메모리 공유로 해결 |
| 서로 다른 프로세스 (독립된 주소공간) | System V 세마포어(semget/semop/semctl), named 세마포어(sem_open) | 맞음 — 커널이 관리하는 공유 리소스에 여러 프로세스가 키/이름으로 접근 |
프로세스 경계를 넘어서 “락 걸림/풀림” 상태를 주고받는 것 자체가 프로세스 간 통신(정보 교환)의 한 형태라서, System V는 세마포어를 메시지 큐 · 공유메모리와 나란히 IPC 3형제로 묶어놓았습니다.
3. 기본 API
| 함수 | 역할 |
|---|---|
semget(key, nsems, flags) | 세마포어 집합 생성/오픈 |
semop(semid, ops, nops) | P/V 연산(대기/신호) 수행 |
semctl(semid, semnum, cmd, ...) | 초기값 설정, 조회, 삭제 등 제어 |
msgget과 마찬가지로 key 기반으로 자원을 식별한다는 패턴은 동일합니다. → ftok과 IPC 키 생성
4. 락 구현의 종류 — 세마포어는 그중 하나
| 분류 | 구현 | 쓰는 상황 |
|---|---|---|
| 상호 배제 | Mutex | 기본 선택. 못 잡으면 잠들고(커널 sleep/wake), 임계 구역이 길어도 무난 |
| 상호 배제 | Spinlock | 못 잡으면 루프를 돌며 확인(busy-wait). 임계 구역이 아주 짧거나 잠들 수 없는 곳(커널·인터럽트). 단일 코어에선 낭비 |
| 상호 배제 | 재진입(Recursive) 락 | 같은 스레드가 여러 번 잡아야 할 때. 남용하면 설계 문제 신호 |
| 읽기/쓰기 구분 | Reader-Writer Lock (pthread_rwlock_t) | 읽기는 동시에, 쓰기는 단독. 읽기가 압도적인 테이블·설정. 임계 구역이 짧으면 mutex보다 느릴 수 있음 |
| 읽기/쓰기 구분 | Seqlock | 읽기는 락 없이 하고, 읽는 중 쓰기가 있었으면 재시도. 쓰기 드물고 데이터 작을 때 |
| 개수/신호 | 세마포어 | 동시 접근 N개 제한(자원 풀), 스레드 간 신호 전달 |
| 개수/신호 | Condition Variable | mutex와 짝으로 “조건이 될 때까지 대기” |
| 개수/신호 | Barrier / Latch | 여러 스레드가 한 지점에서 모이도록 맞춤 |
| 락 프리 | Atomic / CAS (__sync_*, std::atomic) | 카운터·플래그·포인터 교체 |
| 락 프리 | Lock-free 자료구조, RCU | 읽기 비용을 거의 없애고 싶을 때(쓰기는 복사 후 교체) |
| 프로세스 간·분산 | 파일 락(flock/fcntl), 공유메모리 mutex, 분산 락(Redis·ZooKeeper·etcd), DB 락 | 프로세스·서버 경계를 넘을 때 |
선택 순서
1) 락이 필요 없는 구조인가? (스레드별 데이터 분리, 불변 데이터)
2) 단순 값이면 atomic
3) 일반 공유 데이터면 mutex, 읽기 위주면 rwlock(더 극단적이면 RCU/seqlock)
4) 자원 개수 제한·신호 전달이면 세마포어
5) 경합이 병목이면 락 분할(샤딩) 또는 한 스레드가 소유하는 구조(메시지 큐)
프로세스 간 락의 함정(사망 시 복구, PTHREAD_PROCESS_SHARED)은 공유메모리 동시성 — 락 선택과 프로세스 간 함정 참고.
5. 세마포어 vs select/poll/epoll — 헷갈리기 쉬운 쌍
둘 다 “기다린다”는 점은 같지만 목적이 다르다.
| 세마포어 | select / poll / epoll | |
|---|---|---|
| 목적 | 동기화 — 접근 개수 제한, 스레드 간 신호 | I/O 다중화 — 여러 fd 중 준비된 것을 기다림 |
| 질문 | ”지금 들어가도 되나?" | "어느 fd에 읽을/쓸 데이터가 생겼나?” |
| 대상 | 카운터 하나 | fd 여러 개(소켓·파이프·타이머 등) |
| 공유 데이터 보호 | 한다(락의 일종) | 안 한다(대기만 함) |
한 줄 비유: 세마포어는 “들어가도 되는지 허락받는 것”, epoll은 “여러 입구 중 어디에 사람이 왔는지 지켜보는 것”.
두 개념이 만나는 지점: fd 기반 신호
일반 세마포어·조건변수는 epoll로 기다릴 수 없다. 소켓 I/O와 “작업이 들어왔다”는 스레드 간 신호를 한 이벤트 루프에서 같이 기다려야 하면 신호를 fd로 만든다.
| 수단 | 설명 |
|---|---|
eventfd | 커널 카운터 fd. EFD_SEMAPHORE 플래그를 주면 read 1회당 1씩 감소 → 카운팅 세마포어처럼 동작 |
self-pipe / socketpair | 다른 스레드가 pipe에 1바이트를 쓰면 select/poll이 깨어남(eventfd 이전의 고전 방식) |
timerfd, signalfd | 타이머 만료·시그널을 fd로 받아 같은 루프에서 처리 |
→ 자세한 epoll 사용법: epoll — fd가 많아질 때의 대안
핵심
세마포어는 “동시성 제어 도구”이면서 동시에 “IPC”입니다 — 프로세스 경계를 넘어 동작할 때만 IPC로 분류되고, 한 프로세스 내부(스레드 간)에서는 그냥 동시성 도구일 뿐입니다. 락 구현은 세마포어 외에도 mutex·spinlock·rwlock·atomic 등이 있고, select/epoll은 락이 아니라 I/O 대기 도구입니다.